﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace DsaPrimeGenerator.Core
{
    public class PrimeGenerator : IPrimeGenerator
    {
        #region Consts

        private const int SeedLengthInBytes = 20;

        private const int SeedLengthInBits = SeedLengthInBytes*8;

        private static readonly BigInteger Two = new BigInteger(2);

        private static readonly BigInteger TwoPowerLengthInBits = BigInteger.Pow(Two, SeedLengthInBits);

        #endregion

        #region Fields

        private readonly SHA1 _sha1;

        private readonly IPrimalityTest _primalityTest;

        private readonly Random _random;

        #endregion

        #region Constructors

        public PrimeGenerator(IPrimalityTest primalityTest)
        {
            if (ReferenceEquals(primalityTest, null))
            {
                throw new ArgumentNullException("primalityTest");
            }
            _primalityTest = primalityTest;
            _sha1 = SHA1.Create();
            _random = new Random();
        }

        public PrimeGenerator(string hashName, IPrimalityTest primalityTest)
        {
            if (ReferenceEquals(primalityTest, null))
            {
                throw new ArgumentNullException("primalityTest");
            }
            _primalityTest = primalityTest;
            _sha1 = SHA1.Create(hashName);
            _random = new Random();
        }

        #endregion

        #region Methods
        
        public void Dispose()
        {
            _sha1.Dispose();
        }

        public bool Generate(BigInteger seed, int L, out BigInteger p, out BigInteger q)
        {
            byte[] seedArray = seed.ToByteArray();
            if (seedArray.Length != SeedLengthInBytes)
            {
                throw new ArgumentException("Seed length should be equal 20 bytes");
            }

            // get q
            // calcuate u = sha(seed) ^ sha((seed + 1) mod(2^g))
            byte[] second = ((seed + 1) % TwoPowerLengthInBits).ToByteArray();
            byte[] first = _sha1.ComputeHash(seedArray);
            second = _sha1.ComputeHash(second);
            if (first.Length != second.Length)
            {
                throw new Exception();
            }
            for (int i = 0; i < first.Length; ++i)
            {
                first[i] ^= second[i];
            }

            // in first value - result of U
            // create U by setting least significant and most significant bit in 1
            first[0] |= 1;
            first[first.Length - 1] |= 128;
            // add a sign zero byte
            byte[] result = new byte[SeedLengthInBytes + 1];
            first.CopyTo(result, 0);
            q = new BigInteger(result);
            if (!_primalityTest.IsPrime(q))
            {
                p = new BigInteger();
                return false;
            }

            // get p
            int b = (L - 1)%SeedLengthInBits;
            int n = (L - 1)/SeedLengthInBits;
            BigInteger twoPowerLMinusOne = BigInteger.Pow(Two, L - 1);
            for (int counter = 0, offset = 2; counter < 4096; ++counter, offset += (n + 1))
            {
                BigInteger[] v = new BigInteger[n + 1];
                for (int k = 0; k <= n; ++k)
                {
                    byte[] shaComputedHash = _sha1.ComputeHash((seed + offset + k).ToByteArray());
                    if ((shaComputedHash[shaComputedHash.Length - 1] & 0x80) == 0x80)
                    {
                        byte[] temp = new byte[shaComputedHash.Length + 1];
                        shaComputedHash.CopyTo(temp, 0);
                        shaComputedHash = temp;
                    }
                    v[k] = new BigInteger(shaComputedHash) % TwoPowerLengthInBits;
                }

                // calculate w
                BigInteger w = v[0];
                for (int i = 1; i < n; ++i)
                {
                    w += v[i] * BigInteger.Pow(Two, (n - i + 1) * SeedLengthInBits);
                }
                w += (v[n] % Two * b) * BigInteger.Pow(Two, n * SeedLengthInBits);
                // calculate X
                BigInteger x = w + twoPowerLMinusOne;

                // c and try set p
                BigInteger c = x % (Two * q);
                p = x - (c - 1);
                if (p >= twoPowerLMinusOne)
                {
                    if (_primalityTest.IsPrime(p))
                    {
                        return true;
                    }
                }
            }
            p = BigInteger.MinusOne;
            return false;
        }

        public bool Generate(BigInteger seed, out int L, out BigInteger p, out BigInteger q)
        {
            int j = _random.Next(0, 8);
            L = 512 + 64*j;
            return Generate(seed, L, out p, out q);
        }

        public Task<GenerationResult> GenerateAsync(BigInteger seed, int L)
        {
            return Task.Run(() =>
            {
                BigInteger p, q;
                bool result = Generate(seed, L, out p, out q);
                return new GenerationResult(p, q, result);
            });
        }

        #endregion
    }
}
